package com.xh3140.xwidget.app.chart;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.xh3140.xwidget.app.chart.scales.LinearAxisScales;
import com.xh3140.xwidget.app.utils.DimenUtils;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class LineChartView extends BaseCoordinatesView {
  // 折线绘图区域（频繁使用，优化）
  private final RectF mDataLabelRectF = new RectF(0f, 0f, 0f, 0f);
  // 折线图线段路径
  private final Path mPathFillLine = new Path();
  private final Path mPathStrokeLine = new Path();
  // 折线图点路径
  private final Path mPathPoint = new Path();
  // 折线填充画笔
  private final Paint mPaintFillLine = new Paint(Paint.ANTI_ALIAS_FLAG);
  // 折线描边画笔
  private final Paint mPaintStrokeLine = new Paint(Paint.ANTI_ALIAS_FLAG);
  // 折线圆点画笔
  private final Paint mPaintPoint = new Paint(Paint.ANTI_ALIAS_FLAG);
  // 坐标点文本画笔
  private final Paint mPaintPointText = new Paint(Paint.ANTI_ALIAS_FLAG);
  // 虚线画笔
  private final Paint mPaintDashLine = new Paint(Paint.ANTI_ALIAS_FLAG);
  // 数据点圆点半径 px
  private float mDataPointRadius = 5f;
  // 数据点坐标文本显示模式
  private DataPointMode mDataPointMode = DataPointMode.NONE;
  // 贝塞尔曲线显示模式
  private BezierLineMode mBezierLineMode = BezierLineMode.LINE_AND_FILL;
  // 数据点列表
  private List<PointF> mDataPoints = new ArrayList<>();
  // 贝塞尔曲线平滑度
  private float mBezierSmoothness = 0.16f;
  // 贝塞尔曲线控制点
  private List<PointF> mBezierControlPoints = new ArrayList<>();
  // 保留小数点位数（动态）
  private int mDataScaleX = 0;
  private int mDataScaleY = 0;
  // 最大值点（频繁使用，优化）
  private float mMaximumDataY = Float.NaN;
  // 最小值点（频繁使用，优化）
  private float mMinimumDataY = Float.NaN;
  // 平均值点（频繁使用，优化）
  private float mAverageDataY = Float.NaN;
  // 折线填充底部（频繁使用，优化）
  private float mLineFillBottomY = 0f;

  public LineChartView(@NonNull Context context) {
    this(context, null);
  }

  public LineChartView(@NonNull Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
  }


  public LineChartView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
  }

  public LineChartView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    initPaints();
  }

  /**
   * 计算贝塞尔曲线控制点
   */
  private static List<PointF> calculateBezierControlPoints(List<PointF> points, float smoothness) {
    final int size = points.size();
    if (size <= 1) {
      return new ArrayList<>();
    }
    final List<PointF> controls = new ArrayList<>();
    for (int i = 0; i < size; ++i) {
      final PointF pp = points.get(Math.max(0, i - 2));
      final PointF bp = points.get(Math.max(0, i - 1));
      final PointF cp = points.get(i);
      final PointF np = points.get(Math.min(size - 1, i + 1));
      final float lcx = bp.x + (cp.x - pp.x) * smoothness;
      final float lcy = bp.y + (cp.y - pp.y) * smoothness;
      final float rcx = cp.x - (np.x - bp.x) * smoothness;
      final float rcy = cp.y - (np.y - bp.y) * smoothness;
      controls.add(new PointF(lcx, lcy));
      controls.add(new PointF(rcx, rcy));
    }
    return controls;
  }

  /**
   * 获取文本宽度 px
   */
  private static int getTextWidth(@NonNull Paint paint, @NonNull String text) {
    return (int) Math.ceil(paint.measureText(text));
  }

  /**
   * 获取字体推荐高度 px
   */
  private static int getFontHeight(@NonNull Paint paint) {
    return (int) Math.ceil(paint.getFontMetrics().descent - paint.getFontMetrics().ascent);
  }

  private void initPaints() {
    // 折线填充画笔
    mPaintFillLine.setColor(Color.CYAN);
    mPaintFillLine.setAlpha(64);
    mPaintFillLine.setStyle(Paint.Style.FILL);
    // 折线描边画笔
    mPaintStrokeLine.setColor(Color.DKGRAY);
    mPaintStrokeLine.setStrokeWidth(2f);
    mPaintStrokeLine.setStyle(Paint.Style.STROKE);
    // 折线圆点画笔
    mPaintPoint.setColor(Color.DKGRAY);
    mPaintPoint.setStyle(Paint.Style.FILL);
    // 坐标点文本画笔
    mPaintStrokeLine.setColor(Color.BLACK);
    mPaintStrokeLine.setStrokeWidth(2f);
    mPaintStrokeLine.setTextSize(DimenUtils.sp2px(getContext(), 9));
    // 虚线画笔
    mPaintDashLine.setStrokeWidth(3f);
    mPaintStrokeLine.setStyle(Paint.Style.STROKE);
    mPaintDashLine.setPathEffect(new DashPathEffect(new float[]{8f, 10f, 8f, 10f}, 0f));
  }

  /**
   * 设置数据点圆点半径 px
   */
  public void setDataPointRadius(float radius) {
    mDataPointRadius = radius;
  }

  /**
   * 设置数据点坐标文本显示模式
   */
  public void setDataPointMode(@NonNull DataPointMode mode) {
    mDataPointMode = mode;
  }

  /**
   * 设置贝塞尔曲线显示模式
   */
  public void setBezierLineMode(@NonNull BezierLineMode mode) {
    mBezierLineMode = mode;
  }

  /**
   * 设置贝塞尔曲线平滑度
   * 值越大，连线越弯曲，为0时，连线为直线，通常取0.16
   */
  public void setBezierLineSmoothness(float smoothness) {
    mBezierSmoothness = smoothness;
  }

  /**
   * 设置数据坐标点
   */
  public void setDataPoints(@NonNull List<PointF> list) {
    mDataPoints = list;
    mBezierControlPoints = calculateBezierControlPoints(list, mBezierSmoothness);
    if (list.isEmpty()) {
      mMaximumDataY = Float.NaN;
      mMinimumDataY = Float.NaN;
      mAverageDataY = Float.NaN;
    } else {
      mDataScaleX = 0;
      mDataScaleY = 0;
      mMaximumDataY = list.get(0).y;
      mMinimumDataY = list.get(0).y;
      mAverageDataY = 0f;
      for (PointF point : list) {

        mDataScaleX = Math.max(mDataScaleX, BigDecimal.valueOf(point.x).scale());
        mDataScaleY = Math.max(mDataScaleY, BigDecimal.valueOf(point.y).scale());
        mMaximumDataY = Math.max(mMaximumDataY, point.y);
        mMinimumDataY = Math.min(mMinimumDataY, point.y);
        mAverageDataY += point.y;
      }
      mAverageDataY /= list.size();
    }
  }

  /**
   * 设置Y轴刻度
   * 设定的值只是建议值，是最终取值得依据
   * 分别采用2进制和[M*10^N]（包含10进制）来划分，然后取最近的值
   *
   * @param minimum 刻度的建议最小值
   * @param maximum 刻度的建议最大值
   * @param count   划分建议数量
   */
  public void setAxisScalesY(float minimum, float maximum, int count) {
    final int scale;
    final float interval;
    final float interval0 = (maximum - minimum) / count;
    final int power1 = (int) Math.round(Math.log(interval0) / Math.log(2));
    final float interval1 = (float) Math.pow(2, power1);
    final int power2 = (int) Math.floor(Math.log10(interval0));
    final int times2 = (int) Math.round(interval0 / Math.pow(10, power2));
    final float interval2 = (float) (times2 * Math.pow(10, power2));
    if (Math.abs(interval0 - interval1) < Math.abs(interval0 - interval2)) {
      scale = Math.max(0, -power1);
      interval = interval1;
    } else {
      scale = Math.max(0, -power2);
      interval = interval2;
    }
    final float minimum0 = (float) (Math.floor(minimum / interval - 5) * interval);
    final float maximum0 = (float) (Math.ceil(maximum / interval + 5) * interval);
    final int count0 = (int) Math.ceil((maximum0 - minimum0) / interval);
    setAxisScalesY(new LinearAxisScales(minimum0, interval, count0, 3, scale));
  }

  /**
   * 设置数据坐标点并自动设置Y轴刻度列表
   */
  public void setDataPointsAutoAxisScalesY(@NonNull List<PointF> list) {
    setDataPoints(list);
    final int maxGridCount = (int) Math.floor(mMainRectF.height() / mGridsBasicKeepY);
    final int count = Math.max(5, Math.min(mDataPoints.size(), maxGridCount));
    setAxisScalesY(mMinimumDataY, mMaximumDataY, count);
  }

  /**
   * 更新与绘图和布局有关的参数
   */
  @Override
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    // 右侧数据标签区域
    mDataLabelRectF.left = mMainRectF.right - 30f;
    mDataLabelRectF.top = mMainRectF.top;
    mDataLabelRectF.right = getWidth() - getPaddingEnd();
    mDataLabelRectF.bottom = mMainRectF.bottom;
    // 折线填充底部（频繁使用，优化）
    final int index = (int) ((mMinimumDataY - mAxisScalesY.start) / mAxisScalesY.interval);
    mLineFillBottomY = mAxisScalesY.getValue(Math.max(0, index - 2));
  }

  @Override
  protected void onThreadDraw(Canvas canvas) {
    super.onThreadDraw(canvas);
    drawLine(canvas);
    drawDash(canvas);
  }

  /**
   * 获取数据点文字相对数据点的漂浮方向
   */
  private boolean getDrawTextDirection(int index) {
    final float beforeY = mDataPoints.get(index == 0 ? index + 1 : index - 1).y;
    final float currentY = mDataPoints.get(index).y;
    final float nextY = mDataPoints.get(index == mDataPoints.size() - 1 ? index - 1 : index + 1).y;
    // 升型
    if (currentY >= beforeY && currentY <= nextY) return false;
    // 降型
    if (currentY >= nextY && currentY <= beforeY) return true;
    // 峰型
    if (beforeY <= currentY && currentY >= nextY) return true;
    // 谷型
    if (beforeY >= currentY && currentY <= nextY) return false;
    // 默认在上方
    return true;
  }

  /**
   * 绘制折现和数据点
   */
  private void drawLine(Canvas canvas) {
    canvas.save();
    canvas.clipRect(mDrawRectF);
    // 重置路径
    mPathFillLine.reset();
    mPathStrokeLine.reset();
    mPathPoint.reset();
    mPaintPointText.setColor(Color.BLACK);
    final float lineFillBottomY = getCanvasY(mLineFillBottomY);
    for (int index = 0; index < mDataPoints.size(); ++index) {
      final PointF currentPoint = getCanvasPointF(mDataPoints.get(index));
      // 添加贝塞尔曲线数据
      if (index == 0) {
        mPathFillLine.moveTo(currentPoint.x, lineFillBottomY);
        mPathFillLine.lineTo(currentPoint.x, currentPoint.y);
        mPathStrokeLine.moveTo(currentPoint.x, currentPoint.y);
      } else {
        final PointF leftControlPoint = getCanvasPointF(mBezierControlPoints.get(index * 2 - 2));
        final PointF rightControlPoint = getCanvasPointF(mBezierControlPoints.get(index * 2 - 1));
        mPathFillLine.cubicTo(leftControlPoint.x, leftControlPoint.y, rightControlPoint.x, rightControlPoint.y, currentPoint.x, currentPoint.y);
        mPathStrokeLine.cubicTo(leftControlPoint.x, leftControlPoint.y, rightControlPoint.x, rightControlPoint.y, currentPoint.x, currentPoint.y);
        // 最后闭合曲线
        if (index == mDataPoints.size() - 1) {
          mPathFillLine.lineTo(currentPoint.x, lineFillBottomY);
          mPathFillLine.close();
        }
      }
      // 添加数据点圆点数据
      mPathPoint.addCircle(currentPoint.x, currentPoint.y, mDataPointRadius, Path.Direction.CCW);
      // 绘制数据点文本
      if (mDataPointMode != DataPointMode.NONE) {
        final String pointText;
        switch (mDataPointMode) {
          case ONLY_X:

            pointText = String.format(String.format("%.%df", mDataScaleX), mDataPoints.get(index).x);
            break;
          case ONLY_Y:
            pointText = String.format(String.format("%.%df", mDataScaleY), mDataPoints.get(index).y);
            break;
          case X_AND_Y:
            pointText = String.format(String.format("(%.%df,%.%df)", mDataScaleX, mDataScaleY), mDataPoints.get(index).x, mDataPoints.get(index).y);
            break;
          default:
            pointText = "";
            break;
        }
        if (pointText.length() > 0) {
          final float drawTextX = currentPoint.x - getTextWidth(mPaintPointText, pointText) / 2;
          final float drawTextY;
          if (getDrawTextDirection(index)) {
            drawTextY = currentPoint.y - 15f;
          } else {
            drawTextY = currentPoint.y + getFontHeight(mPaintPointText) / 2 + 15f;
          }
          canvas.drawText(pointText, drawTextX, drawTextY, mPaintPointText);
        }
      }
    }
    // 绘制折线
    switch (mBezierLineMode) {
      case FILL:
        canvas.drawPath(mPathFillLine, mPaintFillLine);
        break;
      case LINE:
        canvas.drawPath(mPathStrokeLine, mPaintStrokeLine);
        break;
      default:
        canvas.drawPath(mPathFillLine, mPaintFillLine);
        canvas.drawPath(mPathStrokeLine, mPaintStrokeLine);
        break;
    }
    // 绘制圆点
    canvas.drawPath(mPathPoint, mPaintPoint);
  }

  /**
   * 绘制虚线
   */
  private void drawDash(Canvas canvas) {
    if (Float.isNaN(mMaximumDataY) || Float.isNaN(mMinimumDataY) || Float.isNaN(mAverageDataY)) {
      return;
    }
    final float maximumY = getCanvasY(mMaximumDataY);
    final float minimumY = getCanvasY(mMinimumDataY);
    final float averageY = getCanvasY(mAverageDataY);
    // 绘制虚线
    mPaintDashLine.setColor(Color.RED);
    canvas.drawLine(mMainRectF.left, maximumY, mMainRectF.right, maximumY, mPaintDashLine);
    mPaintDashLine.setColor(Color.BLUE);
    canvas.drawLine(mMainRectF.left, minimumY, mMainRectF.right, minimumY, mPaintDashLine);
    mPaintDashLine.setColor(Color.GREEN);
    canvas.drawLine(mMainRectF.left, averageY, mMainRectF.right, averageY, mPaintDashLine);
    // 绘制虚线文本
    canvas.restore();
    canvas.clipRect(mDataLabelRectF);
    // 绘制最大值
    mPaintPointText.setColor(Color.RED);
    final String maximumValueText = String.format(String.format("%.%df", mDataScaleY), mMaximumDataY);
    drawDataLabel(canvas, "Max", maximumValueText, maximumY);
    // 绘制最小值
    mPaintPointText.setColor(Color.BLUE);
    final String minimumValueText = String.format(String.format("%.%df", mDataScaleY), mMinimumDataY);
    drawDataLabel(canvas, "Min", minimumValueText, minimumY);
    // 绘制平均值
    mPaintPointText.setColor(Color.GREEN);
    final String averageValueText = String.format(String.format("%.%df", mDataScaleY + 1), mAverageDataY);
    drawDataLabel(canvas, "Avg", averageValueText, averageY);
  }

  /**
   * 重置方法，绘制数据标签（最大值、最小值、平均值）
   */
  private void drawDataLabel(Canvas canvas, String text, String valueText, float currentY) {
    final float valueTextY = currentY + getFontHeight(mPaintPointText) + 5f;
    canvas.drawText(text, mMainRectF.right - 15f, currentY - 5f, mPaintPointText);
    canvas.drawText(valueText, mMainRectF.right - 15f, valueTextY, mPaintPointText);
  }

  /**
   * 数据点文本显示模式
   *
   * @sample NONE 不显示任何文本
   * @sample ONLY_X 只显示X坐标文本
   * @sample ONLY_Y 只显示Y坐标文本
   * @sample X_AND_Y 显示X坐标和Y坐标文本
   */
  enum DataPointMode {NONE, ONLY_X, ONLY_Y, X_AND_Y}

  /**
   * 贝塞尔曲线显示模式
   *
   * @sample LINE 只显示贝塞尔曲线
   * @sample FILL 只填充贝塞尔曲线
   * @sample LINE_AND_FILL 显示和填充贝塞尔曲线
   */
  enum BezierLineMode {LINE, FILL, LINE_AND_FILL}
}
